寫遊戲一定會遇到「重複做的事」:五隻怪輪流報名、回合制要一格一格跑、清單要一筆一筆印。總不能複製貼上五十行 console.log——所以今天學迴圈:把同一段程式交給電腦跑好幾次。
const enemies = ["雜魚", "小怪", "菁英怪", "小BOSS", "魔王"];
for (let i = 0; i < enemies.length; i++) {
console.log(i, "→", enemies[i]);
}
for 後面括號裡有三段,用分號隔開,執行順序是:
let i = 0 — 起點,整個迴圈只執行這一次i < enemies.length — 每一輪開始前先檢查,通過才跑內容i++ — 每一輪跑完之後才執行輸出:
0 → 雜魚
1 → 小怪
2 → 菁英怪
3 → 小BOSS
4 → 魔王
起點是 0、條件用「嚴格小於 <」、終點用 length——這三個湊在一起,i 剛好掃過 0 到 4,也就是這個陣列的每一個合法索引,一個不多、一個不少。這不是巧合,是語言設計故意配好的。還記得 Day 5 說的「length 本身剛好是第一個不存在的索引」嗎?今天這行程式就是那個知識的實際應用。
while 只保留三段裡的「檢查」,起點和 i++ 都要自己管:
let i = 0;
while (i < 5) {
console.log(i);
i++;
}
輸出是 0、1、2、3、4,跟 for 版本一樣。

但「自己管」就是危險的來源。下一節是我的親身經歷。
上面的 while 我一開始忘記寫 i++:
let i = 0;
while (i < 5) {
console.log(i);
// i++; ← 忘了這行
}
i 永遠是 0,0 < 5 永遠成立,迴圈永遠不會停。按下執行後的現象依序是:
0,旁邊有一個數字在飛快跳動——那是 Chrome 的重複計數器:同一則訊息連續出現時,Chrome 不會真的畫出幾十萬行,而是折疊成一行,用數字顯示「這則訊息重複了幾次」
關掉之前,計數器停在 106082——代表那一行 console.log(i) 在幾秒之內被執行了超過十萬次,而 i 從頭到尾都是 0。如果 Chrome 沒有折疊重複訊息,我的主控台會真的被畫出十萬行。
這次當機讓我體會到兩件事:
while 的自由是有代價的:for 的三段式至少把起點、條件、i++ 寫在同一行,一眼能檢查;while 把它們拆開,忘一步就是這種下場。寫 while 時我會多檢查一次「迴圈裡有沒有東西在往結束的方向前進」hp < 30 和 i < 5 其實是同一條規則修好迴圈之後,我發現一件事:Day 4 學的 hp < 30,當 hp 剛好是 30 時會被判成 false,走 else;今天的 i < 5,當 i 剛好是 5 時也是 false,迴圈結束——兩個其實是同一條規則:「剛好等於界線的值,不算通過」。
但一開始我預期的結束數字不是 5,是 6。追了一下才知道自己漏想了一步:i++ 只有在「這一輪有成功執行」之後才會跑。最後一次成功的輪次是 i = 4,然後 i++ 把它變成 5,接著檢查 5 < 5 失敗——迴圈直接結束,檢查失敗之後就沒有機會再 i++ 了。所以 i 的最終值是 5,永遠不會變成 6。
我在 while 裡每次 i++ 之後多印一次,親眼驗證了這件事:
let i = 0;
while (i < 5) {
console.log(i, "→", enemies[i]);
i++;
console.log(i);
}
輸出的結尾是:
4 → 魔王
5 ← i 的「遺言」:它曾經變成 5,但 5 那一輪從來沒有執行
順手在主控台直接驗證了那個比較式:
console.log(5 < 5); // false
對照兩個迴圈的輸出時,我又冒出一個疑問:for 的最後一次印出是 4,while 的最後一次印出是 5——是兩個迴圈停下來的位置不同嗎?
追查後發現又是「印的位置」搞的鬼:for 的 console.log(i) 在 i++ 之前,while 的第二個 console.log(i) 在 i++ 之後。兩個迴圈的 i 最後其實都是 5,只是 for 的 5 沒有機會被印出來。
想證明這件事,我把 for 的計數器改成 j,並把宣告搬到迴圈外面,想直接在迴圈結束後印出它的值。沒想到連踩了兩個坑:
第一次,我照原本的寫法把 i 改成 j(let j = 0 還是寫在 for 括號裡),結果主控台跳出錯誤:ReferenceError: j is not defined。原來寫在 for 括號裡的 let j 只活在迴圈裡面,迴圈一結束它就消失了,外面的 console.log 根本找不到 j——這個錯誤反而直接證明了「迴圈裡宣告的變數,迴圈外問不到」。
第二次,我在上面加了一行 let j; 來宣告(本來想寫 let j = 0,手滑把 = 0 刪掉),錯誤確實消失了,但印出來的是 undefined,不是數字!
再追查才發現:加了 let j; 之後變成外面一個 j、迴圈裡又一個 j,兩個名字相同但完全不同——迴圈裡所有跑 0 到 5 的動作都是裡面那個 j 在做,外面那個從頭到尾沒被賦值過。
宣告了卻沒被放進任何東西的變數,值就是 undefined。這種現象叫遮蔽(shadowing):內層的同名變數暫時擋住了外層的,叫「j」的指令都先找內層那位。
而且這次的教訓很特別:用「加宣告」消掉錯誤,不代表修對了。大聲的 ReferenceError 消失了,換來的是安靜的 undefined——不報錯的錯,往往更難查。
最後把 for 括號裡的 let 拿掉,讓迴圈直接用外面那個 j,才終於印出:
for 結束後的 j: 5
兩個迴圈的內部結局一模一樣:計數器都走到 5 停下,只是 for 的 5 死得無聲無息。

途中我還發現一個偷吃步:如果連外面的 let j; 都不寫,直接 for (j = 0; ...) 居然也跑得動,一樣印出 5。
查了才知道,對一個從來沒被宣告過的變數賦值,JS 不會報錯,而是默默自動生出一個全域變數——這叫隱式全域(implicit global)。
雖然能用,但這是不推薦的寫法:變數會悄悄掛到整個網頁上,誰都碰得到,而且 JS 後來新增的「嚴格模式」底下這種寫法會直接報錯——它是 1995 年誕生時的歷史包袱,現代標準一直在勸退它。
乖乖宣告還是比較安全。
回頭看,這也解釋了為什麼「起點 0 + 嚴格小於 + length」這個組合不會出界:如果 i < 5 真的會跑到 6,那 enemies[6] 就是 undefined,這種 bug 會到處都是——幸好它不會。
今天搞懂的事:
for 三段式:起點只跑一次、條件每輪開始前檢查、i++ 每輪結束後執行< + length,剛好掃過每個合法索引,這是刻意設計的組合while 更自由但要自己管起點和前進,忘記 i++ 就是無窮迴圈< 的規則全語言一致:剛好等於界線的值不算通過;i 的最終值會停在界線上(5),但那一輪從未執行let 宣告的變數只活在迴圈裡,外面問不到(大聲的 ReferenceError);宣告同名變數會互相遮蔽,只會留下安靜的 undefined——不報錯的錯更難查寫 QA 測試時我常常要「跑遍每一筆測試資料」,原來工程師是用迴圈做這件事的——明天學函式,把重複的邏輯包成一件工具,不用每次重新寫。